{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*Проект выполнила Мария Еремина*"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Часть 1. Описание набора данных и признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Постановка задачи и ее ценность"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "На данный момент существует множество онлайн-сервисов для определения подозрительных и опасных сайтов, достаточно лишь вбить адрес сайта в строку поиска. Большинство таких сервисов обращаются в крупные базы черных списков: Яндекс, VirusTotal, Роскомнадзор (да-да, там тоже ищут), Google, из которых в каждом более 500000 адресов. Другие же сервисы дополнительно сканируют сам сайт на вирусы, взлом и недобросовестную рекламу, а также проводят поведенческий анализ, имитируя пользователей. Но проблема в том, что мошеннические URL генерируются каждый день, и пока такой сайт \"дойдет\" до блэк-листов, он может навредить не одному пользователю, а проверка на вредоносный код - рисковая и технически затратная процедура. <br/>\n",
    "Довольно часто можно визуально определить ссылку на подозрительный сайт - домен может быть слишком длинным, содержать множество несвязанных символов и т.д. Попробуем решить задачу определения опасного сайта только по его URL."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Источник данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Данные взяты из <a href='https://github.com/faizann24/Using-machine-learning-to-detect-malicious-URLs'>репозитория</a> на Github. Автор собирал адреса из открытых списков опасных и безопасных сайтов."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Описание набора данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Набор данных состоит из двух столбцов:<br/><br/>\n",
    "'<b>url</b>' - строка с адресом сайта (не содержит названия протокола)<br/>\n",
    "'<b>label</b>' - значение целевой переменной, содержит метки 'good' и 'bad', определяющие безопасен сайт или нет.<br/><br/>\n",
    "Будем решать задачу бинарной классификации."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Часть 2. Первичный анализ данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для начала импортируем необходимые библиотеки:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "\n",
    "from scipy.sparse import *\n",
    "\n",
    "from sklearn.ensemble import RandomForestClassifier\n",
    "from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold, validation_curve, GridSearchCV\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.metrics import roc_auc_score\n",
    "from sklearn.feature_extraction.text import TfidfVectorizer\n",
    "\n",
    "from matplotlib import pyplot as plt\n",
    "import seaborn as sns\n",
    "%matplotlib inline\n",
    "\n",
    "# библиотеки для дальнейшей обработки URL\n",
    "from urllib.parse import urlparse\n",
    "import tldextract\n",
    "import ipaddress as ip\n",
    "import re, string\n",
    "\n",
    "# игнорируем warnings\n",
    "import warnings\n",
    "warnings.filterwarnings(\"ignore\")\n",
    "\n",
    "from pylab import rcParams\n",
    "rcParams['figure.figsize'] = 12, 9\n",
    "rcParams['axes.titlesize'] = 16\n",
    "sns.set(style='darkgrid', palette='deep', color_codes=True, font_scale=1.5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Далее загрузим наши данные, посмотрим на размер набора и первые 10 строк."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data_df = pd.read_csv(\"data/data.csv\")\n",
    "print('Полная выборка содержит {} строк.'.format(data_df.shape[0]))\n",
    "data_df.head(10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим, есть ли пропуски в данных и дублирующиеся строки. Если повторы есть, выведем число дублей, а затем удалим все, кроме первого вхождения."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "count_duplicates = data_df.shape[0] - data_df.drop_duplicates().shape[0]\n",
    "data_df = data_df.drop_duplicates().reset_index(drop=True)\n",
    "\n",
    "print('Полная выборка содержит {} дублирующихся строк.'.format(count_duplicates))\n",
    "print('Количество пропусков:')\n",
    "data_df.isnull().sum()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Отлично, пропусков в данных нет, и мы удалили все повторы.\n",
    "<br>\n",
    "<br>\n",
    "Теперь преобразуем значения целевой переменной `label`. Обозначим `good` = 0 и `bad` = 1, так как предсказывать мы будем именно \"плохие сайты\"."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data_df['label'] = data_df['label'].map({'bad': 1, 'good': 0})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на распределение целевого класса `label`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data_df['label'].value_counts()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видим несбалансированность классов - \"плохих\" строк в 5 раз меньше, чем \"хороших\"."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Также на этом этапе выполним небольшую предобработку - поделим все наши данные на обучающую и тестовую выборки - 70% и 30% данных соответственно. Используем для этого метод `train_test_split` со следующими параметрами:\n",
    " - 'stratify' = data_df['label']. Необходимо для соотнесения количества целевых меток в выборках.\n",
    " - 'random_state' = 17\n",
    " - 'test_size' = 0.3\n",
    " - 'shuffle' = True\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train, test = train_test_split(data_df, random_state = 17, \n",
    "                               test_size = 0.3, shuffle = True, \n",
    "                               stratify = data_df['label'])\n",
    "print('Тренировочная выборка:')\n",
    "print('Класс 0 -  {}, класс 1 - {}.'.format(train['label'].value_counts()[0], train['label'].value_counts()[1]))\n",
    "print('Тестовая выборка:')\n",
    "print('Класс 0 -  {}, класс 1 - {}.'.format(test['label'].value_counts()[0], test['label'].value_counts()[1]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Часть 3. Первичный визуальный анализ"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Так как на данном этапе у нас всего лишь один признак - строка URL. Попробуем выделить из нее следующие вещи:\n",
    "<br>\n",
    "1. Длина строки. В длинном URL легче спрятать подозрительные элементы и перенаправления.\n",
    "2. Длина домена. \n",
    "3. Число цифр в строке. В хороших названиях сайтов редко используются цифры, только если они не значимы для названия, например, год или код города. Чем меньше цифр и больше осмысленных слов, тем понятнее куда мы переходим.\n",
    "4. Является ли домен IP-адресом. Если вместо домена используется IP, то возможно кто-то пытается похитить данные пользователей.\n",
    "5. Количество субдоменов.\n",
    "6. Число редиректов (количество '//').\n",
    "7. Содержит ли строка адреса запрос (query).\n",
    "8. Символ '@'. Все, что находится перед этим символом, браузер игнорирует, и настоящая ссылка может быть скрыта от пользователя.\n",
    "9. Количество слэшей ('/'). Чем меньше папок, тем лучше.\n",
    "10. Есть ли в имени домена буквы в верхнем регистре.\n",
    "<br>\n",
    "На этом пока остановимся.   \n",
    "<br>\n",
    "Воспользуемся библиотеками <a href='https://docs.python.org/3/library/urllib.parse.html'>urllib</a> и <a href='https://pypi.org/project/tldextract/'>tldextract</a>, чтобы распарсить URL. На примере одной строки посомтрим, что вернут методы `urlparse` и `tldextract.extract`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "urlparse(train['url'].iloc[56])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tldextract.extract(train['url'].iloc[56])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# метод для выяснения, является ли строка IP-адресом\n",
    "def is_ip_address(domain):\n",
    "    try:\n",
    "        if ip.ip_address(domain):\n",
    "            return 1\n",
    "    except:\n",
    "        return 0\n",
    "    \n",
    "# метод, принимающий строку и возвращающий количество элементов, \n",
    "# разделенных точкой (количество субдоменов)\n",
    "def count_subdomains(subdomain):\n",
    "    if subdomain.count('.') > 0:\n",
    "        return subdomain.count('.') + 1\n",
    "    elif len(subdomain) > 0:\n",
    "        return 1\n",
    "    else:\n",
    "        return 0 "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# заведем новый DataFrame и скопируем в него train\n",
    "new_df = train.copy()\n",
    "\n",
    "# заведем 2 столбца для хранения URL после парсинга\n",
    "new_df['extract'] = new_df['url'].apply(tldextract.extract)\n",
    "new_df['urlparse'] = new_df['url'].apply(urlparse)\n",
    "\n",
    "# посчитаем длину домена и самого URL\n",
    "new_df['url_length'] = new_df['url'].apply(len)\n",
    "new_df['domain_length'] = new_df['extract'].apply(lambda x: len(x.domain))\n",
    "\n",
    "# число цифр в домене\n",
    "new_df['count_digits_in_domain'] = new_df['extract'].apply(lambda x: len(re.findall('(\\d+)', x.domain)))\n",
    "\n",
    "# число субдоменов\n",
    "new_df['count_subdomains'] = new_df['extract'].apply(lambda x: count_subdomains(x.subdomain))\n",
    "\n",
    "new_df['domain_is_ip'] = new_df['extract'].apply(lambda x: is_ip_address(x.domain))\n",
    "\n",
    "# подсчитаем число символов '@', '/', '//'\n",
    "new_df['count_slash'] = new_df['url'].apply(lambda x: x.count('/'))\n",
    "new_df['count_d_slash'] = new_df['url'].apply(lambda x: x.count('//'))\n",
    "new_df['count_@'] = new_df['url'].apply(lambda x: x.count('@'))\n",
    "\n",
    "# есть ли query\n",
    "new_df['have_query'] = new_df['urlparse'].apply(lambda x: 0 if x.query == '' else 1)\n",
    "\n",
    "# буквы в верхнем регистре в домене\n",
    "caps = re.compile('[A-Z]+')\n",
    "new_df['have_caps'] = new_df['extract'].apply(lambda x: 1 if len(caps.findall(x.domain)) > 0 else 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.kdeplot(new_df[new_df['label'] == 1]['url_length'], shade=True, color='r')\n",
    "sns.kdeplot(new_df[new_df['label'] == 0]['url_length'], shade=True)\n",
    "plt.title(\"Длина URL\", fontsize=15)\n",
    "plt.legend(['Класс 1', 'Класс 0']); "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.kdeplot(new_df[new_df['label'] == 1]['domain_length'], shade=True, color='r')\n",
    "sns.kdeplot(new_df[new_df['label'] == 0]['domain_length'], shade=True)\n",
    "plt.title(\"Длина домена\", fontsize=15)\n",
    "plt.legend(['Класс 1', 'Класс 0']); \n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.kdeplot(new_df[new_df['label'] == 1]['count_subdomains'], shade=True, color='r')\n",
    "sns.kdeplot(new_df[new_df['label'] == 0]['count_subdomains'], shade=True)\n",
    "plt.title(\"Количество субдоменов\", fontsize=15)\n",
    "plt.legend(['Класс 1', 'Класс 0']); "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.kdeplot(new_df[new_df['label'] == 1]['count_digits_in_domain'], shade=True)\n",
    "sns.kdeplot(new_df[new_df['label'] == 0]['count_digits_in_domain'], shade=True)\n",
    "plt.title(\"Количество цифр в домене\", fontsize=15)\n",
    "plt.legend(['Класс 1', 'Класс 0']); "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.kdeplot(new_df[new_df['label'] == 1]['count_slash'], shade=True, color='r')\n",
    "sns.kdeplot(new_df[new_df['label'] == 0]['count_slash'], shade=True)\n",
    "plt.title(\"Количество '/'\", fontsize=15)\n",
    "plt.legend(['Класс 1', 'Класс 0']);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.factorplot(x='label', y='count_d_slash', data=new_df, kind='bar', size=6)\n",
    "plt.title(\"Количество перенаправлений ('//')\", fontsize=15);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.factorplot(x='label', y='count_@', data=new_df, kind='bar', size=6)\n",
    "plt.title(\"Количество символов @\", fontsize=15);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.kdeplot(new_df[new_df['label'] == 1]['count_digits_in_domain'], shade=True, color='r')\n",
    "sns.kdeplot(new_df[new_df['label'] == 0]['count_digits_in_domain'], shade=True)\n",
    "plt.title(\"Количество цифр в домене\", fontsize=15)\n",
    "plt.legend(['Класс 1', 'Класс 0']);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.factorplot(x='label', y='domain_is_ip', data=new_df, kind='bar', size=6)\n",
    "plt.title(\"Домен - IP\", fontsize=15);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.factorplot(x='label', y='have_query', data=new_df, kind='bar', size=6)\n",
    "plt.title(\"Наличие query\", fontsize=15);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.factorplot(x='label', y='have_caps', data=new_df, kind='bar', size=6)\n",
    "plt.title(\"Верхний регистр в домене\", fontsize=15);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.stripplot(x=train['label'], y=train['url'].apply(len), size=8)\n",
    "plt.title('URL length')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "На графике видно, что чем больше URL, тем вероятнее, что он ведет на подозрительный сайт. Размер самой длинной строки - почти 1700 символов."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Часть 4. Инсайты, найденные зависимости"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- Пропусков в данных не обнаружено, удалены 9000 повторяющихся записей. \n",
    "- Признаки недобросовестных URL выделяют их на графиках. \n",
    "- На всех графиках есть длинный хвост у распределения класса 1 - это связано с природой данных. \n",
    "- Длинные URL имеют большую степень опасности, чем URL с длинными доменами.\n",
    "- Наличие символов в верхнем регистре и замена домена IP-адресом однозначно определяют класс 1. Проблема в том, что объектов с этими признаками очень малое количество относительно выборки.\n",
    "- График количества знака '/' показал, что страницы, представляющие угрозу, чаще всего пытаются упрятать как можно дальше.\n",
    "- Собаки уже практически не используются в составлении адресов, но все еще используются мошенниками.\n",
    "- Так как адреса сайтов - текстовые данные, далее есть смысл использовать Tfidf.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Часть 5. Выбор метрики"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для оценки качества алгоритма подойдет метрика ROC-AUC - площадь (Area Under Curve) под кривой ошибок (Receiver Operating Characteristic curve). Эта метрика не очень хорошо обобщается для многоклассовой классификации, а для бинарной классификации подходит. Также она зависит от предсказанных вероятностей классов, а не от «жёсткой» дискретной классификации, что хорошо для выборок с дисбалансом классов как в случае этой задачи."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Часть 6. Выбор модели"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В части 3 уже были рассмотрены 9 признаков и возможно было бы использовать деревья решений или их обобщения (случайный лес и градиентный бустинг). Но URL - текстовое поле, поэтому целесообразней преобразовать адреса при помощи Tfidf и обучить логистическую регрессию, а уже после добавлять выявленные признаки и смотреть на качество на кросс-валидации.Убедимся в этом на примере. Разделим new_df на обучающую и валидационную выборки в размере 70% и 30%, соблюдая стратификацию, выделим целевой признак, а затем обучим случайный лес, зафиксировав `random_state` = 17. Оценим качество на валидационной выборке."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "new_df.drop(['url', 'extract', 'urlparse'], axis=1, inplace=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train, X_valid, y_train, y_valid = train_test_split(new_df.drop('label', axis=1), new_df['label'], \n",
    "                                                    test_size=0.3, random_state=17, stratify=new_df['label'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tree = RandomForestClassifier(random_state=17).fit(X_train, y_train)\n",
    "y_tree_pred = tree.predict(X_valid)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('Случайный лес. ROC-AUC: {}'.format(roc_auc_score(y_valid, y_tree_pred)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь обучим логистическую регрессию на тех же данных. Среди фич есть неотмасштабированные признаки, что скажется на качестве."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "logit = LogisticRegression(random_state=17).fit(X_train, y_train)\n",
    "y_logit_pred = logit.predict(X_valid)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('Логистическая регрессия на 9 признаках. ROC-AUC: {}'.format(roc_auc_score(y_valid, y_logit_pred)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как и ожидалось, результат чуть лучше случайного выбора. Преобразуем URL-адреса в матрицу Tfidf  и повторим разделение данных, обучение и оценку качества логистической регрессии."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train, X_valid, y_train, y_valid = train_test_split(train['url'], new_df['label'], \n",
    "                                                      test_size=0.3, random_state=17, stratify=new_df['label'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# обучим Tfidf с униграммами на тренировочной выборке, а затем преобразуем тренировочную и валидационную выборки\n",
    "tfidf = TfidfVectorizer(ngram_range=(1, 1), max_features=50000)\n",
    "X_train_tfidf = tfidf.fit_transform(X_train)\n",
    "X_valid_tfidf = tfidf.transform(X_valid)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "logit = LogisticRegression(random_state=17).fit(X_train_tfidf, y_train)\n",
    "y_tfidf_pred = logit.predict(X_valid_tfidf)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('Логистическая регрессия на Tfidf. ROC-AUC: {}'.format(roc_auc_score(y_valid, y_tfidf_pred)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как видно, качество повысилось на 20% в сравнении со случайным лесом. И это без подбора гиперпараметров Tfidf и логистической регресиии."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Часть 7. Предобработка данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tfidf учитывает вклады слов, но не учитывает их порядок. Это не имеет значения в контексте предложения, но для URL порядок составных частей важен. <img src='../../img/URL.png'>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. Используемый протокол\n",
    "2. Имя хоста\n",
    "3. Субдомен\n",
    "4. Доменное имя\n",
    "5. Домен верхнего уровня (суффикс)\n",
    "6. Путь\n",
    "7. Параметр и значение"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Поделим URL на 4 части - имя хоста, доменное имя, suffix и путь."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "url_df = pd.DataFrame()\n",
    "\n",
    "# парсим строку как и в предыдущий раз\n",
    "url_df['extract'] = train['url'].apply(tldextract.extract)\n",
    "url_df['urlparse'] = train['url'].apply(urlparse)\n",
    "\n",
    "# достаем 4 текстовых признака\n",
    "url_df['hostname'] = url_df['urlparse'].apply(lambda x: x.path.split('/')[0])\n",
    "url_df['suffix'] = url_df['extract'].apply(lambda x: x.suffix.replace('.', ' '))\n",
    "url_df['path'] = url_df['urlparse'].apply(lambda x: ''.join([s + ' ' for s in x.path.split('/')[1:]]).strip())\n",
    "url_df['domain'] = url_df['extract'].apply(lambda x: x.domain)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим первые 10 строк."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "url_df.drop(['extract', 'urlparse'], axis=1).head(10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Поделим выборки на обучающую и валидационную."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train, X_valid, y_train, y_valid = train_test_split(url_df.drop(['extract', 'urlparse'], axis=1), \n",
    "                                                      new_df['label'], \n",
    "                                                      test_size=0.3, \n",
    "                                                      random_state=17, \n",
    "                                                      stratify=new_df['label'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Обучим на четырех получившихся признака обучающей выборки Tfidf и преобразуем валидационную, затем соберем все в sparse_matrix."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tfidf = TfidfVectorizer(ngram_range=(1, 1), max_features=50000)\n",
    "train_1 = tfidf.fit_transform(X_train['hostname'])\n",
    "valid_1 = tfidf.transform(X_valid['hostname'])\n",
    "train_2 = tfidf.fit_transform(X_train['suffix'])\n",
    "valid_2 = tfidf.transform(X_valid['suffix'])\n",
    "train_3 = tfidf.fit_transform(X_train['path'])\n",
    "valid_3 = tfidf.transform(X_valid['path'])\n",
    "train_4 = tfidf.fit_transform(X_train['domain'])\n",
    "valid_4 = tfidf.transform(X_valid['domain'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_sparse = csr_matrix(hstack([train_1, train_2, train_3, train_4]))\n",
    "X_valid_sparse = csr_matrix(hstack([valid_1, valid_2, valid_3, valid_4]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Обучим логистическую регрессию с параметрами по умолчанию и проверим качество."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "logit = LogisticRegression(random_state=17).fit(X_train_sparse, y_train)\n",
    "y_tfidf_4_pred = logit.predict(X_valid_sparse)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('Логистическая регрессия на Tfidf с 4 признаками. ROC-AUC: {}'.format(roc_auc_score(y_valid, y_tfidf_4_pred)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Качество повысилось."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Количество данных и распределение целевого класса в тренировочной выборке."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "url_df.drop(['extract', 'urlparse'], axis=1, inplace=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train['label'].shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train['label'].value_counts()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Также на этом этапе выделим отложенную выборку, отдадим ей 30% строк, и метки классов поместим в отдельную переменную."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X, y = url_df, train['label']\n",
    "X_train, X_holdout, y_train, y_holdout = train_test_split(X, y, test_size=0.2, \n",
    "                                                        random_state=17, \n",
    "                                                        stratify=y)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Размеры отложенной и обучающей выборок после разбиения."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train.shape, X_holdout.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Заново обучим Tfidf на 4 признаках тренировочной выборки и соединим их в разреженные матрицы."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tfidf = TfidfVectorizer(ngram_range=(1, 1), max_features=50000)\n",
    "train_1 = tfidf.fit_transform(X_train['hostname'])\n",
    "holdout_1 = tfidf.transform(X_holdout['hostname'])\n",
    "train_2 = tfidf.fit_transform(X_train['suffix'])\n",
    "holdout_2 = tfidf.transform(X_holdout['suffix'])\n",
    "train_3 = tfidf.fit_transform(X_train['path'])\n",
    "holdout_3 = tfidf.transform(X_holdout['path'])\n",
    "train_4 = tfidf.fit_transform(X_train['domain'])\n",
    "holdout_4 = tfidf.transform(X_holdout['domain'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_sparse = csr_matrix(hstack([train_1, train_2, train_3, train_4]))\n",
    "X_holdout_sparse = csr_matrix(hstack([holdout_1, holdout_2, holdout_3, holdout_4]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Часть 8. Кросс-валидация и настройка гиперпараметров модели"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Кросс-валидация"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "При разбиении на фолды необходимо сохранить распределение классов. Это особенно важно, если в классах изначально есть сильный дисбаланс. Для это используем `sklearn.model_selection.StratifiedKFold`. Разбиение будет на 3 фолда. Обязательно фиксируем значение seed для воспроизводимости результата."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "skf = StratifiedKFold(random_state=17, n_splits=3, shuffle=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для кросс-валидации подойдет метод `cross_val_score` с параметрами `X=X_train_sparse`, `y=y_train`, `cv=skf`, `scoring='roc_auc'`. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Настройка гипермпараметров модели"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для настройки гиперпараметров понадобится `GridSearchCV`. Подбирать будем параметр `C` из `np.logspace(-3, 3, 5)` - силу регуляризации, чем больше коэффициент, тем меньше регуляризация. Помимио этого, нужно будет выставить параметр `class_weight` = 'balanced'. `Random_state` оставим равным 17. Сначала проведем кросс-валидацию с параметрами по умолчанию, затем приступим к настройке, для сравнимости результатов."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "np.mean(cross_val_score(logit, X=X_train_sparse, y=y_train, cv=skf, scoring='roc_auc'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "logit = LogisticRegression(random_state=17, class_weight='balanced')\n",
    "params = {'C': np.logspace(-3, 3, 5)}\n",
    "grid = GridSearchCV(logit, param_grid=params, cv=skf, scoring='roc_auc').fit(X_train_sparse, y_train)\n",
    "print('Качество на кросс-валидации: {}'.format(grid.best_score_))\n",
    "print('Оптимальное значение: {}'.format(grid.best_params_))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно яное переобучение, так как для проведения кросс-валидации было отобрано мало данных, а именно объектов с прогнозируемым классом (их вообще мало). Проверим качество на отложенной выборке."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "logit = LogisticRegression(random_state=17, class_weight='balanced', C=31.0).fit(X_train_sparse, y_train)\n",
    "roc_auc_score(y_holdout, logit.predict(X_holdout_sparse))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Настройка параметров Tfidf"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Настроим параметры `ngram` = униграммы и биграммы, `max_features` = range(50000, 100001, 10000). Качество посмотрим на отложенной выборке."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scores = []\n",
    "for n in [1, 2]:\n",
    "    for max_feat in range(50000, 100001, 10000):\n",
    "        tfidf = TfidfVectorizer(ngram_range=(1, n), max_features=max_feat)\n",
    "        train_1 = tfidf.fit_transform(X_train['hostname'])\n",
    "        holdout_1 = tfidf.transform(X_holdout['hostname'])\n",
    "        train_2 = tfidf.fit_transform(X_train['suffix'])\n",
    "        holdout_2 = tfidf.transform(X_holdout['suffix'])\n",
    "        train_3 = tfidf.fit_transform(X_train['path'])\n",
    "        holdout_3 = tfidf.transform(X_holdout['path'])\n",
    "        train_4 = tfidf.fit_transform(X_train['domain'])\n",
    "        holdout_4 = tfidf.transform(X_holdout['domain'])\n",
    "        X_train_sparse_cv = csr_matrix(hstack([train_1, train_2, train_3, train_4]))\n",
    "        X_holdout_sparse_cv = csr_matrix(hstack([holdout_1, holdout_2, holdout_3, holdout_4]))\n",
    "        logit = LogisticRegression(random_state=17, class_weight='balanced', C=31.0).fit(X_train_sparse_cv, y_train)\n",
    "        scores.append(roc_auc_score(y_holdout, logit.predict(X_holdout_sparse_cv)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scores"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "np.argmax(scores)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Лучшее значение было достигнуто с параметрами `ngram` = (1, 2) и `max_features` = 70000."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# метод для преобразования тренировочной и отложенной выборки\n",
    "def to_tfidf(train_x, test_x):\n",
    "    tfidf = TfidfVectorizer(ngram_range=(1, 2), max_features=70000)\n",
    "    train_1 = tfidf.fit_transform(train_x['hostname'])\n",
    "    holdout_1 = tfidf.transform(test_x['hostname'])\n",
    "    train_2 = tfidf.fit_transform(train_x['suffix'])\n",
    "    holdout_2 = tfidf.transform(test_x['suffix'])\n",
    "    train_3 = tfidf.fit_transform(train_x['path'])\n",
    "    holdout_3 = tfidf.transform(test_x['path'])\n",
    "    train_4 = tfidf.fit_transform(train_x['domain'])\n",
    "    holdout_4 = tfidf.transform(test_x['domain'])\n",
    "    X_train_sparse_cv = csr_matrix(hstack([train_1, train_2, train_3, train_4]))\n",
    "    X_holdout_sparse_cv = csr_matrix(hstack([holdout_1, holdout_2, holdout_3, holdout_4]))\n",
    "    return X_train_sparse_cv, X_holdout_sparse_cv"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Часть 9. Создание новых признаков."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Процесс создания и логическое обоснование приведено в части 3. Будем добавлять бинарные признаки по одному к нашей выборке."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X, y = pd.concat([url_df, new_df], axis=1), train['label']\n",
    "X_train, X_holdout, y_train, y_holdout = train_test_split(X, y, test_size=0.2, \n",
    "                                                            random_state=17, \n",
    "                                                            stratify=y)\n",
    "X_new_train, X_new_holdout = to_tfidf(X_train, X_holdout)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Метрика без новых признаков."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "logit = LogisticRegression(random_state=17, class_weight='balanced', C=31.0).fit(X_train, y_train)\n",
    "roc_auc_score(y_holdout, logit.predict(X_holdout))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tfidf + Domain is IP"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "full_X_train = csr_matrix(hstack([X_new_train, X_train[['domain_is_ip']]]))\n",
    "full_X_test = csr_matrix(hstack([X_new_holdout, X_holdout[['domain_is_ip']]]))\n",
    "logit = LogisticRegression(random_state=17, class_weight='balanced', C=31.0).fit(full_X_train, y_train)\n",
    "roc_auc_score(y_holdout, logit.predict(full_X_test))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tfidf + Верхний регистр"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "full_X_train = csr_matrix(hstack([X_new_train, X_train[['have_caps']]]))\n",
    "full_X_test = csr_matrix(hstack([X_new_holdout, X_holdout[['have_caps']]]))\n",
    "logit = LogisticRegression(random_state=17, class_weight='balanced', C=31.0).fit(full_X_train, y_train)\n",
    "roc_auc_score(y_holdout, logit.predict(full_X_test))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tfidf + Наличие query в строке"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "full_X_train = csr_matrix(hstack([X_new_train, X_train[['have_query']]]))\n",
    "full_X_test = csr_matrix(hstack([X_new_holdout, X_holdout[['have_query']]]))\n",
    "logit = LogisticRegression(random_state=17, class_weight='balanced', C=31.0).fit(full_X_train, y_train)\n",
    "roc_auc_score(y_holdout, logit.predict(full_X_test))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Можно заметить, что добавление новых признаков либо ухудшает модель, либо улучшает на десятитысячные доли, поэтому можно оставить так, как есть."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Часть 10. Прогноз для тестовой выборки"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для начала приведем тестовую и тренировочные выборки к нужному виду."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# преобразовываем train\n",
    "train['extract'] = train['url'].apply(tldextract.extract)\n",
    "train['urlparse'] = train['url'].apply(urlparse)\n",
    "train['hostname'] = train['urlparse'].apply(lambda x: x.path.split('/')[0])\n",
    "train['suffix'] = train['extract'].apply(lambda x: x.suffix.replace('.', ' '))\n",
    "train['path'] = train['urlparse'].apply(lambda x: ''.join([s + ' ' for s in x.path.split('/')[1:]]).strip())\n",
    "train['domain'] = train['extract'].apply(lambda x: x.domain)\n",
    "\n",
    "y_train = train['label']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# преобразовываем test\n",
    "test['extract'] = test['url'].apply(tldextract.extract)\n",
    "test['urlparse'] = test['url'].apply(urlparse)\n",
    "test['hostname'] = test['urlparse'].apply(lambda x: x.path.split('/')[0])\n",
    "test['suffix'] = test['extract'].apply(lambda x: x.suffix.replace('.', ' '))\n",
    "test['path'] = test['urlparse'].apply(lambda x: ''.join([s + ' ' for s in x.path.split('/')[1:]]).strip())\n",
    "test['domain'] = test['extract'].apply(lambda x: x.domain)\n",
    "\n",
    "y_test = test['label']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train, test = to_tfidf(train, test)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "logit = LogisticRegression(random_state=17, class_weight='balanced', C=31.0).fit(train, y_train)\n",
    "print('Результат ROC_AUC на тестовой выборке: {}'.format(roc_auc_score(y_test, logit.predict(test))))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Часть 11. Выводы"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Метрика ROC-AUC для данного алгоритма на тестововой выборке - 0.95. Скор на тесте выше, чем на отложенной выборке. <br>\n",
    "На кросс-валидации модель переобучилась, но показала хороший скор на тестовой выборке. <br>\n",
    "Что в дальнейшем может улучшить решение данной задачи:\n",
    "- Найти более полные URL\n",
    "- Найти больше плохих адресов для приближения к балансу классов\n",
    "- Использовать в качестве признаков порт, протокол, IP."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
